凯撒密码 - CS50x 2023
对于这个问题,你将实现一个程序,使用凯撒密码加密消息,如下所示。
$ ./caesar 13
plaintext: HELLO
ciphertext: URYYB
开始
打开 VS Code。
首先点击终端窗口,然后直接输入 cd
。 你应该发现它的“提示符”类似于下面。
单击该终端窗口内部,然后执行
wget https://cdn.cs50.net/2022/fall/psets/2/caesar.zip
然后按 Enter 键,以便在你的 codespace 中下载一个名为 caesar.zip
的 ZIP 文件。 请务必注意 wget
和 URL 之间的空格,以及其他任何字符,不要遗漏!
现在执行
来创建一个名为 caesar
的文件夹。 现在你可以执行
并在提示符下回复“y”,然后按 Enter 键,以删除你下载的 ZIP 文件。
现在输入
然后按 Enter 键,进入该目录。 你的提示符现在应该类似于下面。
如果一切顺利,你应该执行
并看到一个名为 caesar.c
的文件。 执行 code caesar.c
应该会打开该文件,你将在其中键入此问题集的代码。 如果没有,请回溯你的步骤,看看你是否可以确定你哪里出错了!
背景
据说凯撒(没错,就是那位凯撒)过去常常通过将每个字母移动一定的位数来“加密”(也就是用可逆的方式隐藏)机密消息。 例如,他可能会将 A 写成 B,将 B 写成 C,将 C 写成 D,…,并且在字母表中回绕,将 Z 写成 A。因此,为了对某人说 HELLO,凯撒可能会写 IFMMP 代替。 收到凯撒的此类消息后,收件人必须通过将字母沿相反方向移动相同的位数来“解密”它们。
这种“密码系统”的秘密在于只有凯撒和收件人知道一个秘密,即凯撒移动他的字母的位数(例如,1)。 按照现代标准,这并不算特别安全。不过,如果你是世界上第一个这么做的人,那可就相当安全了!
未加密的文本通常称为 plaintext(明文 (原文 plaintext))。 加密的文本通常称为 ciphertext(密文 (原文 ciphertext))。 使用的密钥称为 key(密钥)。
为了清楚起见,以下是如何使用密钥 1
加密 HELLO
得到 IFMMP
:
加载中,请稍候...
plaintext | H | E | L | L | O |
---|---|---|---|---|---|
+ key | 1 | 1 | 1 | 1 | 1 |
= ciphertext | I | F | M | M | P |
更正式地说,凯撒算法(即密码)通过将每个字母“旋转”k 个位置来加密消息。 更正式地说,如果p 是某个明文(即未加密的消息),pi 是 p 中的字符,并且 k 是一个密钥(即非负整数),那么密文 c 中的每个字母 ci,计算公式为 这里的 意味着“对26取模”,也就是求除以26的余数。这个公式可能使密码看起来比实际更复杂,但它实际上只是精确表达算法的一种简洁方式。为了方便理解,我们可以把A(或 a)看作0,B(或 b)看作1,以此类推,Z(或 z)就是25。假设凯撒想用密钥3偷偷地告诉别人“Hi”。因此,如果他的明文是“Hi”,那么“H”(也就是7)会变成“K”,而“i”(也就是8)会变成“L”。明白了吗?
现在,我们来编写一个名为 caesar
的程序,它可以让你用凯撒密码加密信息。用户在运行程序时,需要通过命令行参数来指定密钥,这个密钥将用于加密运行时输入的秘密信息。我们不能假定用户输入的密钥一定是数字,但如果用户输入的是数字,我们可以假定它是一个正整数。
以下是该程序可能工作方式的一些示例。例如,如果用户输入密钥 1
和明文 HELLO
:
$ ./caesar 1
plaintext: HELLO
ciphertext: IFMMP
如果用户提供密钥 13
和明文 hello, world
,则该程序的工作方式如下:
$ ./caesar 13
plaintext: hello, world
ciphertext: uryyb, jbeyq
注意,逗号和空格不会被加密,只有字母会被旋转!
再来一个例子怎么样?如果用户再次提供密钥 13
,并提供更复杂的明文,则该程序的工作方式如下:
$ ./caesar 13
plaintext: be sure to drink your Ovaltine
ciphertext: or fher gb qevax lbhe Binygvar
为什么?
请注意,原始消息的大小写已保留。小写字母保持小写,大写字母保持大写。
如果用户输入了非数字的命令行参数会怎么样?该程序应提醒用户如何使用该程序:
$ ./caesar HELLO
Usage: ./caesar key
如果用户没有提供任何命令行参数呢?该程序应提醒用户如何使用该程序:
$ ./caesar
Usage: ./caesar key
如果用户提供了多个命令行参数呢?该程序应提醒用户如何使用该程序:
$ ./caesar 1 2 3
Usage: ./caesar key
观看视频
规范
设计并实现一个名为 caesar
的程序,用于使用凯撒密码加密消息。
- 请在
caesar
目录下创建一个名为caesar.c
的文件来实现你的程序。 - 你的程序必须接受一个单独的命令行参数,一个非负整数。我们姑且称之为 。
- 如果程序在没有命令行参数或多于一个命令行参数的情况下运行,应使用
printf
打印错误信息,并立即从main
函数返回值1
(表示错误)。 - 如果命令行参数中包含非十进制数字的字符,程序应打印
Usage: ./caesar key
并从main
函数返回值1
。 - 请勿假定 小于或等于 26。程序应能处理所有小于 的非负整数值。
- 也就是说,即使使用者输入的数值过大,超出
int
类型的范围(int
可能会溢出),你也不必担心程序会因此崩溃。 - 但是,即使 大于 ,程序输入和输出中的字母字符都应保持字母的性质。
- 例如,如果 为 ,即使根据 asciitable.com,反斜杠
\
在 ASCII 码表中距离A
有 个位置,A
也不应变为\
,而应循环移位变为B
。 - 程序应输出
plaintext:
(后接两个空格,无换行),然后提示用户使用get_string
函数输入明文。 - 程序应输出
ciphertext:
(后接一个空格,无换行),然后输出加密后的密文。明文中的每个字母字符都应“旋转” k 个位置;非字母字符则保持不变。 - 你的程序必须保留大小写:大写字母,即使旋转,也必须保持大写字母;小写字母,即使旋转,也必须保持小写字母。
- 输出密文后,程序应打印一个换行符,并通过从
main
函数返回0
来结束运行。
建议
如何开始? 让我们一步一步地解决这个问题。
伪代码
首先编写,尝试在 caesar.c
中编写一个 main
函数,该函数仅使用伪代码实现该程序,即使不(还!)确定如何用实际代码编写它。
提示
实现方法不止一种,所以这只是一种!
int main(void)
{
// 确保程序只使用一个命令行参数运行
// 确保 argv[1] 中的每个字符都是数字
// 将 argv[1] 从 `string` 转换为 `int`
// 提示用户输入明文
// 对于明文中的每个字符:
// 如果它是字母,则旋转该字符
}
在看到我们的伪代码后,可以编辑你自己的伪代码,但不要简单地将我们的伪代码复制/粘贴到你自己的代码中!
计算命令行参数
无论你的伪代码是什么,让我们首先编写仅检查程序是否使用单个命令行参数运行的 C 代码,然后再添加其他功能。
具体来说,以这样一种方式修改 caesar.c
中的 main
,如果用户没有提供任何命令行参数,或者提供了两个或更多,该函数将打印 "Usage: ./caesar key\n"
然后返回 1
,从而有效地退出程序。 如果用户正好提供一个命令行参数,则该程序不应打印任何内容,而只需返回 0
。 因此,该程序的行为应如下所示。
$ ./caesar
Usage: ./caesar key
$ ./caesar 1 2 3
Usage: ./caesar key
提示
- 回想一下,你可以使用
printf
进行打印。 - 回想一下,函数可以使用
return
返回一个值。 - 回想一下,
argc
包含传递给程序的命令行参数的数量,加上程序本身的名称。
检查密钥
现在你的程序(希望!)正在按规定接受输入,现在是下一步的时候了。
在 caesar.c
中的 main
下方添加一个函数,例如,only_digits
,该函数接受一个 string
作为参数,如果该 string
仅包含数字 0
到 9
,则返回 true
,否则返回 false
。 确保也在 main
上方添加该函数的原型。
提示
你很可能需要一个类似下面的原型:
bool only_digits(string s);
请务必在文件顶部包含
cs50.h
,以便编译器能识别string
和bool
类型。请记住,
string
本质上是一个char
类型的数组。请记住,
strlen
函数(在string.h
中声明)用于计算string
的长度。你可能会发现
isdigit
(在ctype.h
中声明)很有用,请参考 manual.cs50.io。但请注意,它一次只能检查一个char
!
接下来,修改 main
函数,使其调用 only_digits
函数来处理 argv[1]
。如果该函数返回 false
,则 main
应该打印 "Usage: ./caesar key\n"
并返回 1
。否则,main
应该只返回 0
。程序应该表现得如下:
$ ./caesar banana
Usage: ./caesar key
使用密钥
现在,修改 main
函数,使其将 argv[1]
转换为 int
类型。你可能会发现 atoi
(在 stdlib.h
中声明)很有用,请参考 manual.cs50.io。然后使用 get_string
函数提示用户输入明文,提示信息为 "plaintext: "
。
然后,实现一个名为 rotate
的函数,该函数接受一个字符 char
和一个整数 int
作为输入。如果该字符是字母,则将其旋转相应的位数(必要时从 Z
绕回到 A
,z
绕回到 a
);否则,返回原字符。
提示
你很可能需要一个类似下面的原型:
char rotate(char c, int n);
例如,调用
rotate('A', 1)
或者rotate('a', 1)
应该返回'B'
。调用rotate('!', 1)
应该返回'!'
。请记住,你可以使用
(int)
将char
显式“转换”为int
,并使用(char)
将int
转换为char
。或者你可以通过简单地将一个视为另一个来隐式地进行转换。你可能需要从大写字母中减去
'A'
的 ASCII 值,使其以 0 为基准进行计算('A'
为 0,'B'
为 1,以此类推),计算完成后再加回去。类似地,你可能需要从小写字母中减去
'a'
的 ASCII 值,使其以 0 为基准进行计算('a'
为 0,'b'
为 1,以此类推),计算完成后再加回去。你可能会发现
ctype.h
中声明的其他一些函数很有用,请参考 manual.cs50.io。当从像
25
这样的值“绕回”到0
时,你可能会发现%
很有用。
接下来,修改 main
函数,使其打印 "ciphertext: "
,然后遍历用户输入的明文,对每个字符调用 rotate
函数,并打印其返回值。
提示
- 请记住,
printf
可以使用%c
打印一个char
。 - 如果你在调用
printf
时根本看不到任何输出,则很可能是因为你正在打印超出 0 到 127 的有效 ASCII 范围的字符。尝试将字符临时打印为数字(使用%i
而不是%c
)以查看你正在打印的值!
示例演示
如何测试你的代码
执行以下命令以使用 check50
评估代码的正确性。但请务必自己编译并测试它!
check50 cs50/problems/2023/x/caesar
执行以下命令以使用 style50
评估代码的风格。
如何提交
在你的终端中,执行以下命令以提交你的工作。
submit50 cs50/problems/2023/x/caesar